﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Text;

namespace Kyoh.Lib.Component.Arguments
{
    public partial class ArgumentPropertyAttribute
    {
        /// <summary>引数解析用オブジェクトの基本クラスを提供します。</summary>
        /// <typeparam name="T">引数オブジェクトの型を指定します。</typeparam>
        public abstract class ArgumentsParserBase<T>
            where T : class
        {
            private static readonly List<ArgumentPropertyAttribute> attributes;
            private readonly List<ArgumentParser> properties;

            private static readonly TypeConverter defaultConverter = new StringConverter();

            /// <summary>接頭文字を表す表示用の文字列の最大長を取得します。</summary>
            public static int PrefixLength { get; private set; }
            /// <summary>表示用の引数名の最大長を取得します。</summary>
            public static int KeyLength { get; private set; }
            /// <summary>区切り文字を表す表示用の文字列の最大長を取得します。</summary>
            public static int SeparatorLength { get; private set; }

            static ArgumentsParserBase()
            {
                var type = typeof(T);

                attributes = new List<ArgumentPropertyAttribute>();
                var argObj = Attribute.GetCustomAttribute(type, typeof(ArgumentObjectAttribute)) as ArgumentObjectAttribute;

                foreach (var prop in type.GetProperties(BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
                {
                    if (prop.PropertyType == typeof(bool) && !prop.CanRead)
                        continue;

                    var attr = Attribute.GetCustomAttribute(prop, typeof(ArgumentPropertyAttribute)) as ArgumentPropertyAttribute;
                    if (attr == null)
                        continue;

                    if (attributes.Exists(p => p.Match(attr.ArgumentName)))
                        throw new InvalidArgumentException(ArgumentInvalidity.Duplicate) { ArgumentName = attr.ArgumentName, PropertyName = prop.Name, };
                    if (attributes.Exists(p => p.Match(attr.Abbreviation)))
                        throw new InvalidArgumentException(ArgumentInvalidity.Duplicate) { ArgumentName = attr.Abbreviation, PropertyName = prop.Name, };
                    attr.Reffer(argObj, prop);
                    attributes.Add(attr);

                    PrefixLength = Math.Max(PrefixLength, attr.PrefixLength);
                    KeyLength = Math.Max(KeyLength, attr.KeyLength);
                    SeparatorLength = Math.Max(SeparatorLength, attr.SeparatorLength);
                }
            }

            /// <summary><see cref="ArgumentsParserBase{T}"/>の新しいインスタンスを初期化します。</summary>
            public ArgumentsParserBase()
            {
                this.properties = new List<ArgumentParser>();
                foreach (var attr in attributes)
                    this.properties.Add(new ArgumentParser(attr));
            }

            /// <summary>定義済の引数名かどうかを判定します。</summary>
            /// <param name="argument">定義済みの引数名かどうか確かめる文字列。</param>
            /// <returns>文字列が定義済みの引数名にマッチする場合は <c>true</c> 、それ以外の場合は <c>false</c> 。</returns>
            public static bool IsDefinedArgument(string argument)
            {
                return (attributes.Exists(p => p.Match(argument)));
            }

            /// <summary>コマンドラインから渡された引数を解析して、引数のオブジェクトを作成します。</summary>
            /// <param name="args">コマンドラインから渡された引数を指定します。</param>
            /// <param name="obj">引数を設定するオブジェクトを指定します。</param>
            /// <exception cref="ArgumentNullException">必要な引数が設定されていません。</exception>
            /// <returns>解析結果を格納した <see cref="ParsedArguments{T}"/> オブジェクト。</returns>
            public ParsedArguments<T> Parse(string[] args, T obj = null)
            {
                foreach (var prop in this.properties)
                    prop.Reset();

                var ret = new ParsedArguments<T>(obj);
                foreach (var arg in args)
                {
                    bool existsProperty = false;
                    foreach (var prop in this.properties)
                    {
                        if (prop.Parse(arg, obj))
                        {
                            existsProperty = true;
                            ret.AddProcessor(prop.Property, obj);
                            break;
                        }
                    }
                    if (!existsProperty)
                        ret.AddRemaining(arg);
                }

                foreach (var prop in this.properties)
                    if (prop.Required && !prop.Set)
                        throw new InvalidArgumentException(ArgumentInvalidity.NotSet) { ArgumentName = prop.ArgumentName, PropertyName = prop.Property.Name, };
                return ret;
            }

            /// <summary>解析したオブジェクトを引数文字列に変換します。</summary>
            /// <param name="obj">引数を解析して得られた <typeparamref name="T"/> オブジェクト。</param>
            /// <returns>オブジェクトから変換して得られた引数文字列。</returns>
            public static string Argument(T obj)
            {
                var message = new StringBuilder();
                foreach (var prop in attributes)
                    message.Append(prop.Argument(obj)).Append(' ');
                return message.ToString();
            }

            /// <summary>引数の詳細な説明文字列を取得します。</summary>
            /// <returns>引数の詳細な説明文字列。</returns>
            public static string DetailUsage()
            {
                var message = new StringBuilder();
                foreach (var attr in attributes)
                    message.AppendLine(attr.DetailUsage(PrefixLength, KeyLength, SeparatorLength));
                return message.ToString();
            }

            /// <summary>引数の説明文字列を取得します。</summary>
            /// <param name="detail">詳細な説明を追加するかどうかを指定します。</param>
            /// <returns>引数の説明文字列。</returns>
            public static string Usage(bool detail = false)
            {
                var message = new StringBuilder();
                foreach (var attr in attributes)
                    message.Append(attr.Usage()).Append(' ');
                if (detail)
                {
                    message.AppendLine();
                    message.AppendLine();
                    foreach (var attr in attributes)
                        message.AppendLine(attr.DetailUsage(PrefixLength, KeyLength, SeparatorLength));
                }
                return message.ToString();
            }
        }
    }
}